In [1]:
import csv
import snappy
from ast import literal_eval

lets import the khovanov homology of the census knots we computed

In [43]:
R.<t,q> = LaurentPolynomialRing(QQ)

import re
from sage.misc.sage_eval import sage_eval

def parse_poly(s):
    s = str(s).strip().replace("^", "**").replace(" ", "")
    s = re.sub(r'(?<=[0-9])(?=[tq])', '*', s)
    s = re.sub(r'(?<=[tq0-9\)])(?=[tq])', '*', s)
    return R(sage_eval(s, locals={'t': t, 'q': q}))

KH_data = []

with open('/Users/leomousseau/Desktop/khovanov paper code/Khovanov_census_knots_21_05_26.csv', 'r') as file:
    reader = csv.reader(file)
    next(reader)

    for row in reader:
        name = row[0]
        free_string = row[4]      # kh_integral_unreduced
        torsion_string = row[6]   # torsion_unreduced

        if free_string.strip() == "":
            continue

        free_part = parse_poly(free_string)

        torsion_part = {}

        if torsion_string.strip() != "":
            for piece in torsion_string.split("|"):
                match = re.match(r"Torsion\s*of\s*order\s*(\d+)\s*:\s*(.*)", piece.strip())
                order = ZZ(match.group(1))
                poly = parse_poly(match.group(2))
                torsion_part[order] = poly

        khovanov_homology = [free_part, torsion_part]

        KH_data.append([name, khovanov_homology])

len(KH_data)
Out[43]:
785
In [47]:
def KH(i, j, khovanov_homology):
    '''
    input: integers (i,j) and the Khovanov homology of a link given by [free_part, torsion_part]

        free_part is given by a polynomial in variables t and q, such that
        the coefficient of t^i*q^j is the free rank of the Khovanov homology
        in bigrading (i,j).

        torsion_part is a dictionary. Its keys are torsion orders, and its
        values are polynomials in t and q. For example, if
        torsion_part[2] has coefficient 3 at t^i*q^j, then there are
        three copies of Z/2 in bigrading (i,j).

    output: Khovanov homology in bigrading (i,j)
        [free_rank, torsion]

        free_rank is the rank of the free Z-part in bigrading (i,j).
        torsion is a list of torsion orders occurring in bigrading (i,j).

        For example, output [1, [2, 2, 5]] means 
        Z ⊕ Z/2 ⊕ Z/2 ⊕ Z/5.
    '''
    
    free_part, torsion_part = khovanov_homology

    free_rank = free_part.monomial_coefficient(t^i * q^j)

    torsion = []
    for order, poly in torsion_part.items():
        multiplicity = poly.monomial_coefficient(t^i * q^j)
        torsion += [order] * ZZ(multiplicity)

    return [free_rank, torsion]
    free_part, torsion_part = khovanov_homology

    free_rank = free_part.monomial_coefficient(t^i * q^j)

    torsion = []
    for order, poly in torsion_part.items():
        multiplicity = poly.monomial_coefficient(t^i * q^j)
        torsion += [order] * ZZ(multiplicity)

    return [free_rank, torsion]

Lets import some data about the census knots (positivity, genus, fiberedness status)

In [48]:
positive=[]
with open('positivity.csv', 'r') as file:
    reader = csv.reader(file)
    for row in reader:
        positive.append([row[0],row[1]])
positive=positive[1:]
positivity_dict=dict(positive)
len(positive)
Out[48]:
1267
In [49]:
Seifert_genus=[]
with open('3genus.csv', 'r') as file:
    reader = csv.reader(file)
    for row in reader:
        Seifert_genus.append([row[0],row[1]])
Seifert_genus=Seifert_genus[1:]
genus_dict=dict(Seifert_genus)
len(Seifert_genus)
Out[49]:
1267
In [50]:
fibered=[]
with open('fibered.csv', 'r') as file:
    reader = csv.reader(file)
    for row in reader:
        fibered.append([row[0],row[1]])
fibered=fibered[1:]
fibered_dict=dict(fibered)
len(fibered)
Out[50]:
1267

Lets look at all census knots for which we could not find a positive diagram:

In [51]:
positivity_unclear=[]
for k in positive:
    if k[1]!='True':
        positivity_unclear.append(k[0])
In [52]:
len(positivity_unclear)
Out[52]:
480

Let's implement the obstructions to positivity of Khovanov homology discussed in the paper

In [58]:
def positive_obstructions_from_khovanov(khovanov_homology, genus, fibered):
    '''
    Tests the Khovanov homology obstructions (listed below) for being a positive knot.

    Input:
        khovanov_homology = [free_part, torsion_part]
        genus = Seifert genus
        fibered = True or False

    Output:
        list of obstruction reasons.
        Empty list means: not obstructed by these tests.

    lets apply the following obstruction to positivity from khovanov homology:
    (1) KH^(i,j)=0 if i<0
    (2) KH^(0,2g-1)=F where g is the genus of K (for a link -Eulercharacteristic)
    (3) KH^(0,2g+1)=F 
    (4) KH^(1,2g+1)=F^p1 where p1 is the chromatic number of the reduced Seifert graph of a positive diagram
    (5) KH^(1,2g+1)=0 if and only if K is fibered
    (6) KH^(0,j)=0 for all other j
    (7) KH^(1,j)=0 for all other j
    (8) KH^(i,2g-1)=0 for all other i
    (9) KH^(0,2g+1)=0 for all other i
    
    '''
    free_part, torsion_part = khovanov_homology

    g = ZZ(genus)
    is_fibered = bool(fibered)

    j_low  = 2*g - 1
    j_high = 2*g + 1

    bad = []

    # -------------------------
    # free part obstructions
    # -------------------------

    for (i, j), coeff in free_part.dict().items():

        if coeff == 0:
            continue

        # (1) Kh^(i,j)=0 if i<0
        if i < 0:
            bad.append(f"free part nonzero at negative i: Kh^({i},{j})")

        # (6) Kh^(0,j)=0 for all other j
        if i == 0 and j not in [j_low, j_high]:
            bad.append(f"extra free part in Kh^(0,{j})")

        # (7) Kh^(1,j)=0 for all other j
        if i == 1 and j != j_high:
            bad.append(f"extra free part in Kh^(1,{j})")

        # (8) Kh^(i,2g-1)=0 for all other i
        if j == j_low and i != 0:
            bad.append(f"extra free part in Kh^({i},{j_low})")

        # (9) should mean Kh^(i,2g+1)=0 for all other i
        if j == j_high and i not in [0, 1]:
            bad.append(f"extra free part in Kh^({i},{j_high})")

    # -------------------------
    # torsion obstructions
    # -------------------------

    for order, poly in torsion_part.items():
        for (i, j), coeff in poly.dict().items():

            if coeff == 0:
                continue

            # (1) Kh^(i,j)=0 if i<0
            if i < 0:
                bad.append(f"Z/{order} torsion at negative i: Kh^({i},{j})")

            # positive knots have prescribed groups in i=0 and i=1.
            # These prescribed groups are free, so torsion there obstructs.
            if i == 0:
                bad.append(f"Z/{order} torsion in Kh^(0,{j})")

            if i == 1:
                bad.append(f"Z/{order} torsion in Kh^(1,{j})")

            # At j = 2g-1 only Kh^(0,2g-1)=Z is allowed
            if j == j_low:
                bad.append(f"Z/{order} torsion in Kh^({i},{j_low})")

            # At j = 2g+1 only possible free groups at i=0,1 are allowed
            if j == j_high:
                bad.append(f"Z/{order} torsion in Kh^({i},{j_high})")

    # -------------------------
    # required groups
    # -------------------------

    # (2) Kh^(0,2g-1)=F, over Z this means free rank 1
    if free_part.monomial_coefficient(t^0 * q^j_low) != 1:
        bad.append(f"Kh^(0,{j_low}) should have free rank 1")

    # (3) Kh^(0,2g+1)=F, over Z this means free rank 1
    if free_part.monomial_coefficient(t^0 * q^j_high) != 1:
        bad.append(f"Kh^(0,{j_high}) should have free rank 1")

    # -------------------------
    # fiberedness condition
    # -------------------------

    rank_Kh1 = sum(
        coeff for (i, j), coeff in free_part.dict().items()
        if i == 1
    )

    torsion_Kh1 = sum(
        coeff for poly in torsion_part.values()
        for (i, j), coeff in poly.dict().items()
        if i == 1
    )

    # (5) Kh^(1,2g+1)=0 iff K is fibered
    if is_fibered:
        if rank_Kh1 != 0 or torsion_Kh1 != 0:
            bad.append("fibered knot: positive would require Kh^(1,*) = 0")
    else:
        if free_part.monomial_coefficient(t^1 * q^j_high) == 0:
            bad.append(f"non-fibered knot: positive would require nonzero Kh^(1,{j_high})")

    return bad

We also want to check whether the census knots are negative using our obstructions (a knot is negative if and only if it admits a diagram with only negative crossings. Notice that a link is negative if and only if its mirror is positive).

So let us write functions which give back the khovanov homology of the mirror knot given the khovanov homology of it:

In [14]:
def mirror_free_poly(P):
    Q = R(0)

    for (i, j), coeff in P.dict().items():
        Q += coeff * t^(-i) * q^(-j)

    return Q

def mirror_torsion_poly(P):
    Q = R(0)

    for (i, j), coeff in P.dict().items():
        Q += coeff * t^(1-i) * q^(-j)

    return Q

def mirror_khovanov_homology(khovanov_homology):
    free_part, torsion_part = khovanov_homology

    mirror_free = mirror_free_poly(free_part)

    mirror_torsion = {
        order: mirror_torsion_poly(poly)
        for order, poly in torsion_part.items()
    }

    return [mirror_free, mirror_torsion]
In [15]:
def positive_or_negative_obstructions_from_khovanov(khovanov_homology, genus, fibered):
    obs_direct = positive_obstructions_from_khovanov(
        khovanov_homology,
        genus,
        fibered
    )

    obs_mirror = positive_obstructions_from_khovanov(
        mirror_khovanov_homology(khovanov_homology),
        genus,
        fibered
    )

    return [obs_direct, obs_mirror]

Let us now apply our obstruction function to the census knots

In [16]:
census_results = []

for name, khom in KH_data:

    g = ZZ(genus_dict[name])
    fib = (fibered_dict[name] == "True")

    diagram_found = (positivity_dict.get(name) == "True")

    obs_direct, obs_mirror = positive_or_negative_obstructions_from_khovanov(khom,g,fib)

    obstructed_positive = (obs_direct != [])
    obstructed_negative = (obs_mirror != [])


    
    if diagram_found:
        status = "positive/negative diagram found"
    elif obstructed_positive and obstructed_negative:
        status = "no diagram found, obstructed"
    else:
        status = "no diagram found, not obstructed"

    census_results.append([
        name,
        status,
        diagram_found,
        obs_direct,
        obs_mirror
    ])

Let us summarize the results:

In [17]:
diagram_found_results = [
    x for x in census_results
    if x[1] == "positive/negative diagram found"
]

obstructed_no_diagram = [
    x for x in census_results
    if x[1] == "no diagram found, obstructed"
]

survivors = [
    x for x in census_results
    if x[1] == "no diagram found, not obstructed"
]

print("Integral unreduced KH checked:", len(census_results))
print("Positive/negative diagram found:", len(diagram_found_results))
print("No positive/negative diagram found:", len(obstructed_no_diagram) + len(survivors))
print("No diagram found, obstructed:", len(obstructed_no_diagram))
print("No diagram found, not obstructed:", len(survivors))

print("\nSurvivors:")
print([x[0] for x in survivors])
Integral unreduced KH checked: 785
Positive/negative diagram found: 319
No positive/negative diagram found: 466
No diagram found, obstructed: 449
No diagram found, not obstructed: 17

Survivors:
['s580', 't04651', 't08387', 't12308', 'o9_09214', 'o9_12773', 'o9_19200', 'o9_22182', 'o9_24667', 'o9_25096', 'o9_30634', 'o9_31294', 'o9_35678', 'o9_38702', 'o9_39519', 'o9_41263', 'o9_43369']

We come to the following conclusion:

Out of the 785 census knots for which the khovanov homology (with Z coefficients) is known, their Khovanov homology obstructs 449 from being positive or negative. Among the remaining 336, there are 319 for which positive (or negative) diagrams were found in [BBD+]. In [BBD+], using different obstructions and data from KnotInfo, the 17 remaining knots were found to be not positive. That means that for the 466 census knots which are not positive and not negative, That means that the Khovanov homology obstructions worked for 449 out of the 466 census knots which are not positive or negative.